package com.shxzz.easy12306.services.ticket.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.text.CharSequenceUtil;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.shxzz.easy12306.framework.starter.base.ApplicationContextHolder;
import com.shxzz.easy12306.framework.starter.cache.StringRedisTemplateProxy;
import com.shxzz.easy12306.framework.starter.cache.toolkit.CacheUtil;
import com.shxzz.easy12306.framework.starter.common.dto.order.*;
import com.shxzz.easy12306.framework.starter.common.dto.pay.RefundPassengerDTO;
import com.shxzz.easy12306.framework.starter.common.dto.pay.RefundReqDTO;
import com.shxzz.easy12306.framework.starter.common.enums.RefundType;
import com.shxzz.easy12306.framework.starter.convention.exceptions.ClientException;
import com.shxzz.easy12306.framework.starter.convention.exceptions.RemoteException;
import com.shxzz.easy12306.framework.starter.convention.exceptions.ServiceException;
import com.shxzz.easy12306.framework.starter.convention.result.Result;
import com.shxzz.easy12306.framework.starter.designpattern.chain.AbstractChainContext;
import com.shxzz.easy12306.framework.starter.user.core.UserContext;
import com.shxzz.easy12306.services.ticket.common.constant.RedisKeyConstant;
import com.shxzz.easy12306.services.ticket.common.enums.SourceEnum;
import com.shxzz.easy12306.services.ticket.common.enums.TicketChainMark;
import com.shxzz.easy12306.services.ticket.dao.entity.TrainDO;
import com.shxzz.easy12306.services.ticket.dao.entity.TrainStationPriceDO;
import com.shxzz.easy12306.services.ticket.dao.entity.TrainStationRelationDO;
import com.shxzz.easy12306.services.ticket.dao.mapper.TrainStationRelationMapper;
import com.shxzz.easy12306.services.ticket.dto.domain.PurchaseTicketPassengerDetailDTO;
import com.shxzz.easy12306.services.ticket.dto.domain.SeatInfoDTO;
import com.shxzz.easy12306.services.ticket.dto.domain.SelectSeatDTO;
import com.shxzz.easy12306.services.ticket.dto.domain.TicketListDTO;
import com.shxzz.easy12306.services.ticket.dto.req.CancelTicketOrderReqDTO;
import com.shxzz.easy12306.services.ticket.dto.req.PurchaseTicketReqDTO;
import com.shxzz.easy12306.services.ticket.dto.req.RefundTicketReqDTO;
import com.shxzz.easy12306.services.ticket.dto.req.TicketPageQueryReqDTO;
import com.shxzz.easy12306.services.ticket.dto.resp.PurchaseTicketRespDTO;
import com.shxzz.easy12306.services.ticket.dto.resp.TicketOrderItemDetailRespDTO;
import com.shxzz.easy12306.services.ticket.dto.resp.TicketPageQueryRespDTO;
import com.shxzz.easy12306.services.ticket.remote.OrderRemoteService;
import com.shxzz.easy12306.services.ticket.remote.PayRemoteService;
import com.shxzz.easy12306.services.ticket.service.SeatService;
import com.shxzz.easy12306.services.ticket.service.TicketService;
import com.shxzz.easy12306.services.ticket.service.TrainService;
import com.shxzz.easy12306.services.ticket.service.TrainStationPriceService;
import com.shxzz.easy12306.services.ticket.service.cache.RegionTrainInfoCacheLoader;
import com.shxzz.easy12306.services.ticket.service.cache.SeatRemainCacheLoader;
import com.shxzz.easy12306.services.ticket.service.cache.StationRegionMappingCacheLoader;
import com.shxzz.easy12306.services.ticket.service.handler.dto.SeatSelectRespDTO;
import com.shxzz.easy12306.services.ticket.service.handler.select.SeatSelector;
import com.shxzz.easy12306.services.ticket.service.handler.tokenbucket.TicketPurchaseTokenBucket;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.boot.CommandLineRunner;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;

/**
 * 车票接口实现类
 *
 * @author shxzz
 * @create 2024-03-04-20:22
 */
@Slf4j
@RequiredArgsConstructor
@Service
public class TicketServiceImpl implements TicketService, CommandLineRunner {

    private final TrainStationRelationMapper trainStationRelationMapper;

    private final AbstractChainContext abstractChainContext;
    private final StringRedisTemplateProxy stringRedisTemplateProxy;
    private final RedissonClient redissonClient;

    private final StationRegionMappingCacheLoader stationRegionMappingCacheLoader;
    private final RegionTrainInfoCacheLoader regionTrainInfoCacheLoader;
    private final SeatRemainCacheLoader seatRemainCacheLoader;

    private final SeatSelector seatSelector;
    private final TicketPurchaseTokenBucket ticketPurchaseTokenBucket;

    private final TrainStationPriceService trainStationPriceService;
    private final TrainService trainService;
    private final SeatService seatService;
    private final OrderRemoteService orderRemoteService;
    private final PayRemoteService payRemoteService;

    private TicketService ticketService;

    private final Cache<String, ReentrantLock> localLockMap = Caffeine.newBuilder()
            .expireAfterWrite(1, TimeUnit.DAYS)
            .build();

    @Override
    public PurchaseTicketRespDTO purchaseTicket(PurchaseTicketReqDTO requestParam) {
        abstractChainContext.handler(TicketChainMark.TICKET_PURCHASE_CHAIN_FILTER.name(), requestParam);

        String lockKey = CacheUtil.buildKey(RedisKeyConstant.LOCK_PURCHASE_TICKET_PREFIX + requestParam.getTrainId());
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock();
        try {
            return ticketService.executePurchaseTicket(requestParam);
        } finally {
            lock.unlock();
        }
    }

    @Override
    public PurchaseTicketRespDTO purchaseTicketV2(PurchaseTicketReqDTO requestParam) {
        abstractChainContext.handler(TicketChainMark.TICKET_PURCHASE_CHAIN_FILTER.name(), requestParam);
        boolean getTokenResult = ticketPurchaseTokenBucket.getTokenFromBucket(requestParam);
        if (!getTokenResult) {
            throw new ServiceException("余票不足");
        }

        List<Integer> seatTypeList = requestParam.getPassengers().stream()
                .map(PurchaseTicketPassengerDetailDTO::getSeatType)
                .collect(Collectors.toSet())
                .stream()
                .sorted()
                .toList();

        List<ReentrantLock> localLockList = seatTypeList.stream()
                .map(seatType -> {
                    String lockKey = CacheUtil.buildKey(RedisKeyConstant.LOCK_PURCHASE_V2_TICKET_PREFIX, requestParam.getTrainId(), seatType);
                    return localLockMap.get(lockKey, key -> new ReentrantLock(true));
                }).toList();
        List<RLock> distributeLockList = seatTypeList.stream()
                .map(seatType -> {
                    String lockKey = CacheUtil.buildKey(RedisKeyConstant.LOCK_PURCHASE_V2_TICKET_PREFIX, requestParam.getTrainId(), seatType);
                    return redissonClient.getFairLock(lockKey);
                }).toList();
        try {
            // 顺序上锁，避免死锁
            localLockList.forEach(ReentrantLock::lock);
            distributeLockList.forEach(RLock::lock);
            return ticketService.executePurchaseTicket(requestParam);
        } finally {
            distributeLockList.forEach(lock -> {
                try {
                    lock.unlock();
                } catch (Exception ignored) {
                    log.error("购票分布式锁解锁失败,火车ID:{}", requestParam.getTrainId());
                }
            });
            localLockList.forEach(lock -> {
                try {
                    lock.unlock();
                } catch (Exception ignored) {
                    log.error("购票本地锁解锁失败,火车ID:{}", requestParam.getTrainId());
                }
            });
        }
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public PurchaseTicketRespDTO executePurchaseTicket(PurchaseTicketReqDTO requestParam) {
        TrainDO trainDO = trainService.getTrainInfo(requestParam.getTrainId());
        List<SeatSelectRespDTO> seatSelectRespDTOList = seatSelector.selectAndLockSeat(trainDO.getTrainType(), requestParam);

        LambdaQueryWrapper<TrainStationRelationDO> queryWrapper = Wrappers.lambdaQuery(TrainStationRelationDO.class)
                .eq(TrainStationRelationDO::getTrainId, requestParam.getTrainId())
                .eq(TrainStationRelationDO::getDeparture, requestParam.getDeparture())
                .eq(TrainStationRelationDO::getArrival, requestParam.getArrival());
        TrainStationRelationDO trainStationRelationDO = trainStationRelationMapper.selectOne(queryWrapper);
        List<TicketOrderItemCreateReqDTO> ticketOrderItemCreateReqDTOList = seatSelectRespDTOList.stream()
                .map(seatSelectRespDTO -> TicketOrderItemCreateReqDTO.builder()
                        .passengerId(seatSelectRespDTO.getPassengerId())
                        .realName(seatSelectRespDTO.getRealName())
                        .idType(seatSelectRespDTO.getIdType())
                        .idCard(seatSelectRespDTO.getIdCard())
                        .ticketType(seatSelectRespDTO.getUserType())
                        .carriageNumber(seatSelectRespDTO.getCarriageNumber())
                        .phone(seatSelectRespDTO.getPhone())
                        .seatType(seatSelectRespDTO.getSeatType())
                        .seatNumber(seatSelectRespDTO.getSeatNumber())
                        .amount(seatSelectRespDTO.getAmount())
                        .build()
                ).toList();
        TicketOrderCreateReqDTO ticketOrderCreateReqDTO = TicketOrderCreateReqDTO.builder()
                .userId(Long.parseLong(UserContext.getUserId()))
                .username(UserContext.getUsername())
                .trainId(trainDO.getId())
                .trainNumber(trainDO.getTrainNumber())
                .orderTime(LocalDateTime.now())
                .departure(trainStationRelationDO.getDeparture())
                .arrival(trainStationRelationDO.getArrival())
                .departureTime(trainStationRelationDO.getDepartureTime())
                .arrivalTime(trainStationRelationDO.getArrivalTime())
                .ridingDate(trainStationRelationDO.getDepartureTime().toLocalDate())
                .ticketOrderItems(ticketOrderItemCreateReqDTOList)
                .source(SourceEnum.INTERNET.getCode())
                .build();

        Result<String> ticketOrderCreateResult = orderRemoteService.createTicketOrder(ticketOrderCreateReqDTO);
        String orderSn = ticketOrderCreateResult.getData();
        if (!ticketOrderCreateResult.isSuccess() || CharSequenceUtil.isEmpty(orderSn)) {
            throw new RemoteException("远程创建订单异常");
        }

        List<TicketOrderItemDetailRespDTO> ticketOrderDetailDTOList = seatSelectRespDTOList.stream()
                .map(seatSelectRespDTO -> TicketOrderItemDetailRespDTO.builder()
                        .seatType(seatSelectRespDTO.getSeatType())
                        .carriageNumber(seatSelectRespDTO.getCarriageNumber())
                        .seatNumber(seatSelectRespDTO.getSeatNumber())
                        .realName(seatSelectRespDTO.getRealName())
                        .idType(seatSelectRespDTO.getIdType())
                        .idCard(seatSelectRespDTO.getIdCard())
                        .ticketType(seatSelectRespDTO.getUserType())
                        .amount(seatSelectRespDTO.getAmount())
                        .build()
                ).toList();
        return PurchaseTicketRespDTO.builder()
                .orderSn(orderSn)
                .ticketOrderDetails(ticketOrderDetailDTOList)
                .build();
    }

    @Override
    public void cancelTicketOrder(CancelTicketOrderReqDTO requestParam) {
        Result<TicketOrderDetailRespDTO> ticketOrderDetailRespDTOResult = orderRemoteService.queryTicketOrderByOrderSn(requestParam.getOrderSn());
        if (!ticketOrderDetailRespDTOResult.isSuccess()) {
            throw new RemoteException("远程查询订单详情异常");
        }
        TicketOrderDetailRespDTO ticketOrderDetailRespDTO = ticketOrderDetailRespDTOResult.getData();
        Result<Boolean> booleanResult = orderRemoteService.closeTicketOrder(new CloseTicketOrderReqDTO(requestParam.getOrderSn()));
        if (!booleanResult.isSuccess()) {
            throw new ServiceException("远程调用订单关闭失败");
        }
        if (Boolean.FALSE.equals(booleanResult.getData())) {
            throw new ServiceException("订单状态异常");
        }
        Long trainId = ticketOrderDetailRespDTO.getTrainId();
        String departure = ticketOrderDetailRespDTO.getDeparture();
        String arrival = ticketOrderDetailRespDTO.getArrival();
        List<TicketOrderPassengerDetailRespDTO> passengerDetails = ticketOrderDetailRespDTO.getPassengerDetails();
        List<SelectSeatDTO> selectSeatDTOList = BeanUtil.copyToList(passengerDetails, SelectSeatDTO.class);
        seatService.rollbackSeatStatus(trainId.toString(), departure, arrival, selectSeatDTOList);
    }

    @Override
    public Void commonTicketRefund(RefundTicketReqDTO requestParam) {
        Result<TicketOrderDetailRespDTO> ticketOrderDetailRespDTOResult = orderRemoteService.queryTicketOrderByOrderSn(requestParam.getOrderSn());
        if (!ticketOrderDetailRespDTOResult.isSuccess()) {
            throw new RemoteException("调用订单服务失败");
        }
        TicketOrderDetailRespDTO ticketOrderDetailRespDTO = ticketOrderDetailRespDTOResult.getData();
        if (ticketOrderDetailRespDTO == null) {
            throw new ClientException("订单不存在");
        }
        RefundReqDTO refundReqDTO = new RefundReqDTO();
        refundReqDTO.setOrderSn(requestParam.getOrderSn());
        List<TicketOrderPassengerDetailRespDTO> refundPassengers = new ArrayList<>();
        if (RefundType.PARTIAL_REFUND.getType().equals(requestParam.getType())) {
            Set<String> passengerIdRecord = new HashSet<>(requestParam.getSubOrderRecordIdReqList());
            refundPassengers = ticketOrderDetailRespDTO.getPassengerDetails().stream()
                    .filter(each -> passengerIdRecord.contains(each.getId()))
                    .toList();
            refundReqDTO.setRefundType(RefundType.PARTIAL_REFUND);
        } else if (RefundType.FULL_REFUND.getType().equals(requestParam.getType())) {
            refundReqDTO.setRefundType(RefundType.FULL_REFUND);
            refundPassengers = ticketOrderDetailRespDTO.getPassengerDetails();
        }
        refundReqDTO.setRefundDetailReqDTOList(BeanUtil.copyToList(refundPassengers, RefundPassengerDTO.class));
        int refundAmount = refundPassengers.stream().mapToInt(TicketOrderPassengerDetailRespDTO::getAmount).sum();
        refundReqDTO.setRefundAmount(refundAmount);
        Result<Void> refundResult = payRemoteService.commonRefund(refundReqDTO);
        if(!refundResult.isSuccess()) {
            throw new ServiceException("退款失败");
        }
        String trainId = ticketOrderDetailRespDTO.getTrainId().toString();
        String departure = ticketOrderDetailRespDTO.getDeparture();
        String arrival = ticketOrderDetailRespDTO.getArrival();
        List<SelectSeatDTO> selectSeatDTOList = BeanUtil.copyToList(refundPassengers, SelectSeatDTO.class);
        seatService.rollbackSeatStatus(trainId, departure, arrival, selectSeatDTOList);
        return null;
    }

    @Override
    public TicketPageQueryRespDTO search(TicketPageQueryReqDTO requestParam) {
        abstractChainContext.handler(TicketChainMark.TICKET_SEARCH_CHAIN_FILTER.name(), requestParam);

        StringRedisTemplate stringRedisTemplate = stringRedisTemplateProxy.getInstance();

        List<Object> queryStationCodeList = new ArrayList<>(Arrays.asList(requestParam.getFromStation(), requestParam.getToStation()));
        List<Object> regionList = stringRedisTemplate.opsForHash()
                .multiGet(RedisKeyConstant.TRAIN_STATION_REGION_MAPPING, queryStationCodeList);
        long count = regionList.stream().filter(Objects::isNull).count();
        if (count > 0) {
            regionList = stationRegionMappingCacheLoader.getAndLoad(queryStationCodeList);
        }

        List<Object> trainBaseInfoList = stringRedisTemplate.opsForHash()
                .values(CacheUtil.buildKey(RedisKeyConstant.REGION_TRAIN_INFO_PREFIX, regionList.toArray(new Object[0])));
        if (CollUtil.isEmpty(trainBaseInfoList)) {
            trainBaseInfoList = regionTrainInfoCacheLoader.getAndLoad(regionList.get(0), regionList.get(1));
        }
        List<TicketListDTO> trainList = trainBaseInfoList.stream().map(each -> JSON.parseObject((String) each, TicketListDTO.class)).toList();

        for (TicketListDTO eachTrain : trainList) {
            List<TrainStationPriceDO> trainPriceList = trainStationPriceService.getTrainStationPrice(eachTrain.getTrainId(), eachTrain.getDeparture(), eachTrain.getArrival());
            List<SeatInfoDTO> seatInfoList = new ArrayList<>();

            for (TrainStationPriceDO eachTrainPrice : trainPriceList) {
                SeatInfoDTO seatInfoDTO = SeatInfoDTO.builder()
                        .type(eachTrainPrice.getSeatType())
                        .price(new BigDecimal(eachTrainPrice.getPrice()).divide(new BigDecimal(100), 1, RoundingMode.HALF_UP))
                        .build();
                String needKey = CacheUtil.buildKey(RedisKeyConstant.TRAIN_REMAIN_TICKET_PREFIX, eachTrain.getTrainId(), eachTrain.getDeparture(), eachTrain.getArrival());
                Object remainTicket = stringRedisTemplate.opsForHash().get(needKey, eachTrainPrice.getSeatType().toString());
                Integer quantity = Optional.ofNullable(remainTicket)
                        .map(Object::toString)
                        .map(Integer::parseInt)
                        .orElseGet(() -> {
                                    Object cnt = seatRemainCacheLoader.getAndLoadAllTrainRemainTicket(
                                            eachTrain.getTrainId(),
                                            eachTrainPrice.getSeatType().toString(),
                                            eachTrain.getDeparture(),
                                            eachTrain.getArrival());
                                    return Optional.ofNullable(cnt).map(Object::toString).map(Integer::parseInt).orElse(0);
                                }
                        );
                seatInfoDTO.setQuantity(quantity);
                seatInfoList.add(seatInfoDTO);
            }
            eachTrain.setSeatClassList(seatInfoList);
        }
        return TicketPageQueryRespDTO.builder()
                .trainList(trainList)
                .departureStationList(buildDepartureStationList(trainList))
                .arrivalStationList(buildArrivalStationList(trainList))
                .trainBrandList(buildTrainBrandList(trainList))
                .seatClassTypeList(buildSeatClassList(trainList))
                .build();
    }

    private List<String> buildDepartureStationList(List<TicketListDTO> trainList) {
        return trainList.stream().map(TicketListDTO::getDeparture).distinct().toList();
    }

    private List<String> buildArrivalStationList(List<TicketListDTO> trainList) {
        return trainList.stream().map(TicketListDTO::getArrival).distinct().toList();
    }

    private List<Integer> buildTrainBrandList(List<TicketListDTO> trainList) {
        Set<Integer> brandSet = new HashSet<>();
        for (TicketListDTO each : trainList) {
            brandSet.addAll(CharSequenceUtil.split(each.getTrainBrand(), ",").stream().map(Integer::parseInt).toList());
        }
        return brandSet.stream().toList();
    }

    private List<Integer> buildSeatClassList(List<TicketListDTO> trainList) {
        return trainList.stream().flatMap(each -> each.getSeatClassList().stream().map(SeatInfoDTO::getType)).distinct().toList();
    }

    @Override
    public void run(String... args) throws Exception {
        ticketService = ApplicationContextHolder.getBean(TicketService.class);
    }
}
